/*
   (C) 2007 - Michael Ocean - Boston University

  This work is a part of the larger snBench project.  Information
  can be found at: http://csr.bu.edu/snbench

  This program is free software; you can redistribute it and/or
  modify it under the terms of Version 2 of the GNU General Public 
  License.
  
  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  General Public License for more details.
  
  You should have received a copy of the GNU Library General Public
  License along with this library; if not, write to the Free
  Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/**
 * The following is a stub of a Kismet client for use in the
 * snBench.  Kismet can accept many clients and as the protocol
 * lacks the ability to differentiate between responses and 
 * requests, multiple instances should be used instead of attempting
 * to multiplex the result across multiple callers (found that
 * out the hard way).
 * 
 * The protocol is available at:
 * http://svn.kismetwireless.net/code/trunk/docs/DEVEL.client
 * 
 * E.g.,  Imagine you want to know when alerts occur and a list of
 * all nearby APs.  You want two clients:
 * 
 * c1 = new KismetClient(host,port);
 * c2 = new KismetClient(host,port);
 * 
 * c1.SendAlertSubscription(); 
 * c2.SendAPListsubscription();
 * 
 * c1.GetLatest();
 * c2.GetLatest();
 * 
 * @author Michael Ocean
 */
package sxe.sensors;

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Hashtable;

import snbenchlib.Log;
import snbenchlib.Trace;
import snbenchlib.Util;
import snobject.snString;
import snobject.snStruct;

public class KismetClient {
	public static String FIELDS_ALERT= "sec,usec,header,bssid,source,dest,other,channel,text";
	public static String FIELDS_NETWORK= "bssid,type,ssid,quality,signal,noise,bestquality,bestsignal,bestnoise";	
	public static String FIELDS_CLIENT="mac,bssid,type,decrypted,ip,quality,signal,noise,bestquality,bestsignal,bestnoise";
	
	public static enum msgtypes {
		TIME,UNKNOWN,ALERT,NETWORK,CLIENT, ACTIVITY;
		
		public static msgtypes parse(String sMode){
			if(sMode.equals("NETWORK"))
	    		return msgtypes.NETWORK;
	    	else if(sMode.equals("ALERT"))
	    		return msgtypes.ALERT;
	    	else if(sMode.equals("TIME"))
	    		return msgtypes.TIME;
	    	else if(sMode.equals("CLIENT"))
	    		return msgtypes.CLIENT;
	    	else if(sMode.equals("ACTIVITY"))
	    		return msgtypes.ACTIVITY;
	    	
	   		return msgtypes.UNKNOWN;
		}
		
		public static String getFieldsForMsgType(msgtypes msgType){
			switch(msgType){
			case ALERT:
					return FIELDS_ALERT;
			case NETWORK:
					return FIELDS_NETWORK;
			case CLIENT:
					return FIELDS_CLIENT;
			default:
					return "";
			}
		}
	};
	
	protected static int MAXQUEUELENGTH = 20; 	//	how many KismetMessages will be queued	
	
	protected String mode;
    protected String sensorID;
    
    protected Socket socket;
    protected java.io.InputStream inputStream;
    protected java.io.OutputStream outputStream;
      
    protected ListenerThread listener;
    
    // this is a seqNo required by the Kismet communication protocol
    protected long sequenceNo = 1;
    
    /* a separate "queue" for each known/supported Kismet message type.
     * access to these queues is synchronized on the variables themselves */
    protected ArrayList<KismetMessage> alTimeMsgs;
    protected ArrayList<KismetMessage> alAlertMsgs;
    protected ArrayList<KismetMessage> alNetworkMsgs;
    protected ArrayList<KismetMessage> alUnknownMsgs;
    protected ArrayList<KismetMessage> alClientMsgs;
    
    /** the exception is the activity tracking which is also done via hash */
    protected Hashtable<String,KismetMessage> htActivityMsgs;
    
    /* these help keep track of how many messages have been recv'd (and lost) */
    protected long seqNoTime = 1;
    protected long seqNoClient = 1;
    protected long seqNoAlert = 1;
    protected long seqNoNet = 1;
    protected long seqNoUnk = 1;
	private String sensorname;
	
    
    /**
     * -blocking-
     * 
     * @return a string from the kismet server.  A line from the
     * kismet server is delimited by ascii '0010' and ascii '0001'
     * is a quoted string (for network names)
     * 
     * @throws IOException when connectoin is closed prematurely
     */
    protected String readoneline() throws IOException{
		int read;
		boolean bInKismetQuotes;
		StringBuffer buffer = new StringBuffer();
	    bInKismetQuotes=false;

	    // get a string 
	    while(true)
	    {
	    	if( ( read = inputStream.read() ) < 0) 
	    		throw new IOException( "Connection closed" );
	    	
	    	// useful for debugging the character mappings
	    	// as kismet uses some weird char codes for delimiters
	    	//System.out.print("["+(char)read+"=="+read+"]");
	    	
	    	if((read==10)&&(!bInKismetQuotes))
	    		break;
	    	if(read==01)
	    		bInKismetQuotes=!bInKismetQuotes;

	    	buffer.append((char)read);
	    }
	    
    	//System.out.println("{{" + buffer.toString() + "}}");
		return buffer.toString();
    }
    
    
    private class ListenerThread extends Thread {
    	protected boolean bRun;
    	protected Object lock = new Object();
    	    	
    	public void shutdown(){
    		synchronized(lock){
    			bRun = false;
    		}
    	}
    	private boolean checkrun(){
    		synchronized(lock){
    			return bRun;
    		}
    	}
    	    	
    	public void run() {
    		synchronized(lock){
    			bRun = true;
    		}
    		
    	    while(checkrun()) {
	    		try {
	    			ArrayList<KismetMessage>al = null;
	    			String s = readoneline();
	    			
	    			// debug messages
	    			// System.err.println(s);
	    			
	    			KismetMessage reading = new KismetMessage(s,sensorname);
	    			switch(reading.type){
	    		    	case ALERT:
	    		    		al = alAlertMsgs;
	    		    		reading.setSeqNo(seqNoAlert++);
	    		    		break;
	    		    	case NETWORK:
	    		    		al = alNetworkMsgs;
	    		    		reading.setSeqNo(seqNoNet++);
	    	    			synchronized(htActivityMsgs){
	    	    		    	htActivityMsgs.put(reading.getValue("bssid"), reading);
	    	    			}
	    		    		
	    		    		break;
	    		    	case TIME:
	    		    		al= alTimeMsgs;
	    		    		reading.setSeqNo(seqNoTime++);
	    		    		break;
	    		    	case CLIENT:
	    		    		al= alClientMsgs;
	    		    		reading.setSeqNo(seqNoClient++);
	    	    			synchronized(htActivityMsgs){
	    	    		    	htActivityMsgs.put(reading.getValue("mac"), reading);
	    	    			}

	    		    		break;	    		    		
	    		    	case UNKNOWN:
	    		    		al = alUnknownMsgs;
	    		    		reading.setSeqNo(seqNoUnk++);
	    		    		break;
    				}	
    		    	
	    		    synchronized( al ) {
	    		    	al.add(reading);
	    		    	/* throw away old messages when in excess of MAXQUEUELENGTH */
	    		    	if(al.size()>MAXQUEUELENGTH){
	    		    		al.remove(0);
	    		    	}
	    		    	al = null;
	    		    }
	    		} catch( Exception ex ) {
	    			ex.printStackTrace();
	    		    System.exit( 1 );
	    		}
    	    }
    	}
    }
    
	private ArrayList<KismetMessage> getMessageListForType(msgtypes type) {
		switch(type){
    	case ALERT:
    		return alAlertMsgs;
    	case NETWORK:
    		return alNetworkMsgs;
    	case TIME:
    		return alTimeMsgs;
    	case CLIENT:
    		return alClientMsgs;    		
    	case UNKNOWN:
    	default:
    		return alUnknownMsgs;
		}
	}
    
    public KismetClient( String host, int port, String sensorName ) 
    	throws UnknownHostException,
    			IOException 
    {   
    	mode = null;
    	Log.misc.log("KismetClient: initializing -- attempting to connect to kismet client specified in " +
    			"kismetsensors.xml, client:" + host + ":" + port);
    	
    	socket = new Socket( host, port );
    	inputStream = socket.getInputStream();
    	outputStream = socket.getOutputStream();
    	Log.misc.log("KismetClient: connected to:" + host + ":" + port);
    	
    	sensorname = sensorName;
    	
    	alAlertMsgs = new ArrayList<KismetMessage>();
    	alNetworkMsgs = new ArrayList<KismetMessage>();
    	alTimeMsgs = new ArrayList<KismetMessage>();
    	alUnknownMsgs = new ArrayList<KismetMessage>();
    	alClientMsgs = new ArrayList<KismetMessage>();
    	htActivityMsgs = new Hashtable<String, KismetMessage>();
    }

    /**
     * no one should call this constructor
     */
    protected KismetClient(){}

    synchronized public void disconnect(){
    	boolean bShut = false;
    	while(!bShut){
	    	listener.shutdown();
	    	try{
	    		listener.join();
	    		bShut = true;
	    	}catch(InterruptedException ie){
	    		if(!listener.checkrun())
	    			bShut = true;
	    	}
    	}
    	
    	try {
			socket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
    }
    
    synchronized private ArrayList<String> sendString(String s){
    	String t = "!" + String.valueOf(sequenceNo) + " " + s;
    	ArrayList<String> al = new ArrayList<String>();
    	
    	try{
    		Log.misc.log("KismetClient: sending message", Trace.VERBOSE);
    		outputStream.write(t.getBytes());
    		Log.misc.log("KismetClient: sent message", Trace.VERBOSE);
    		
    	}catch(IOException io){
    		return null;
    	}
    	
    	sequenceNo++;
    	return al;
    }

    /**
     * @return either the oldest KismetMessage waiting in the Queue or null
     * 		if none are waiting
     */
    public KismetMessage GetOldestReading(){
    	ensureUp();
    	msgtypes mt = msgtypes.parse(mode);    	
    	ArrayList<KismetMessage>al = getMessageListForType(mt);
    	
    	synchronized( al ) {
    		if(al.size()>0){
    			System.err.println("current size of Kismet msg queue = " + al.size());
    			return al.remove(0);
       		}
    	}
    	return null;
    }


    /**
     * Locate a particular mac address in the client activity and return 
     * the matching KismetMessage of the latest update or returns null if
     * not found
     * 
     * @return
     */
    public KismetMessage Find(String mac){
    	ensureUp();
    	
    	//System.err.println("htClientMsgs.size()==" + htActivityMsgs.size());
    	return htActivityMsgs.get(mac);	

    	/*
    	ArrayList<KismetMessage>al = getMessageListForType(mt);
    	synchronized(al){
    		for(KismetMessage km : al){
    			if(km.getValue("mac") == mac)
    				return km;
    		}
    	}
    	return null;
    	*/
    }
    
    public snStruct GetAllActivity(){
    	ensureUp();
    	snStruct snS = new snStruct();
    	
    	synchronized(htActivityMsgs){
    		for(String k : htActivityMsgs.keySet()){
    			snS.put(new snString(k), htActivityMsgs.get(k).getSnObject());
    		}	
    	}
    	return snS;
    }
    /**
     * @return either the newest KismetMessage waiting in the Queue or null
     * 		if none are waiting
     */
    public KismetMessage GetNewestReading(){
    	ensureUp();
    	msgtypes mt = msgtypes.parse(mode);    	
    	ArrayList<KismetMessage>al = getMessageListForType(mt);
    	
    	synchronized( al ) {
    		int i = al.size(); 
    		if(i>0){
    			return al.remove(i-1);
       		}
    	}
    	return null;
    }

    
    private void ensureUp() {
    	if(listener==null){
	    	Log.misc.log("KismetClient: launching listener",Trace.VERBOSE);
	    	listener = new ListenerThread();
	    	
	    	Log.misc.log("KismetClient: listener inited", Trace.VERBOSE);
	    	listener.setDaemon( true );
	    	listener.start();
	    	Log.misc.log("KismetClient: listener launched", Trace.VERBOSE);
    	}
	}

	public ArrayList<KismetMessage> GetReadings(int maxNumReadings){
    	ensureUp();
		KismetMessage km;
    	ArrayList<KismetMessage> alRet = new ArrayList<KismetMessage>();

    	msgtypes mt = msgtypes.parse(mode);    	
    	ArrayList<KismetMessage>al = getMessageListForType(mt);
    	//StringBuffer sb = new StringBuffer();
    	
    	synchronized( al ) {
	    	for(int i=0;i<maxNumReadings;i++){
	    		if(al.size()>0){
	    			km=al.remove(0);
	    			alRet.add(km);
	    			//sb.append(km.getString() + "\n");
	    		}
	    	}
    	}
    	return alRet;
    }
       
    private void PrintLastWhatever(ArrayList <KismetMessage>al){	
	    KismetMessage km;
	    
	    //System.out.println("in print whatever");
    	synchronized( al ) {
    		//System.out.println("in print whatever locked");
	    	for(int i=0;i<5;i++){
	    		if(al.size()>0){
	    			km=al.remove(0);
	    			System.out.println(km.getString());
	    		}
	    	}
    	}
    }
	private void Print() {
		PrintLastWhatever(alTimeMsgs);
		PrintLastWhatever(alNetworkMsgs);
		PrintLastWhatever(alAlertMsgs);
		PrintLastWhatever(alUnknownMsgs);
		PrintLastWhatever(alClientMsgs);
	}    
    
    public static void main(String[] args) {
    	Hashtable<String,Object> htArgs = Util.parseArgs(args);
    	
    	String sHost = "localhost";
    	int iHostPort = 2501;

    	if(htArgs.containsKey("serverPort")){
    		iHostPort = ((Integer)htArgs.get("serverPort")).intValue();
    	}

    	if(htArgs.containsKey("simulateKismetServer")){
    		// simulates a kismet server
    		
    		return;
    	}

    	if(htArgs.containsKey("serverHost")){
    		sHost = (String)htArgs.get("serverHost");
    	}
    	
    	KismetClient kc;
    	try{
			kc = new KismetClient(sHost,iHostPort,"simulator");
		}catch(Exception e){
			e.printStackTrace();
			return;
		}
		kc.sendString("ENABLE NETWORK bssid,type,ssid\n");
		kc.sendString("ENABLE ALERT sec,usec,header,bssid,source,dest,other,channel,text\n");
		
		while(true){
			for(int i=0;i<1000;i++);
			kc.Print();
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		// ALERT sec,usec,header,bssid,source,dest,other,channel,text
		
		/* NETWORK bssid,type,ssid,beaconinfo,llcpackets,datapackets,
		 * cryptpackets,weakpackets,channel,wep firsttime,lasttime,
		 * atype,rangeip,gpsfixed,minlat,minlon,minalt,minspd,maxlat,
		 * maxlon,maxalt,maxspd,octets,cloaked,beaconrate,maxrate,
		 * manufkey,manufscore,quality,signal,noise,bestquality,
		 * bestsignal,bestnoise,bestlat,bestlon,bestalt,agglat,agglon,
		 * aggalt,aggpoints,datasize,turbocellnid,turbocellmode,
		 * turbocellsat,carrierset,maxseenrate,encodingset,decrypted,
		 * dupeivpackets,bsstimestamp
		 */
		
		
//	      *CAPABILITY: CLIENT bssid,mac,type,firsttime,lasttime,manufkey,manufscore
//	       ,datapackets,cryptpackets,weakpackets,gpsfixed,minlat,minlon,minalt,minspd,maxla
//	       t,maxlon,maxalt,maxspd,agglat,agglon,aggalt,aggpoints,maxrate,quality,signal,noi
//	       se,bestquality,bestsignal,bestnoise,bestlat,bestlon,bestalt,atype,ip,datasize,ma
//	       xseenrate,encodingset,decrypted,wep

//		!5 ENABLE CLIENT bssid,mac,type,decrypted,ip,quality,signal,noise,bestquality,bestsignal,bestnoise
		
	}

    public void setMode(String sMode) throws Exception{
    	if(sMode.equals(mode))
    		return;
    	mode = sMode;
    	if(sMode.equals("NETWORK"))
    		sendString("ENABLE NETWORK " + FIELDS_NETWORK + "\n");
    	else if(sMode.equals("ALERT"))
    		sendString("ENABLE ALERT " + FIELDS_ALERT + "\n");
    	else if(sMode.equals("TIME"))
    		return;
    	else if(sMode.equals("CLIENT")){
    		sendString("ENABLE CLIENT " + FIELDS_CLIENT + "\n");
    	}
    	else if(sMode.equals("ACTIVITY")){
    		/* activity is both client and network notifications */
    		sendString("ENABLE CLIENT " + FIELDS_CLIENT + "\n");
    		sendString("ENABLE NETWORK " + FIELDS_NETWORK + "\n");
    	}
    	else
    		throw new Exception("unknown mode for KismetSensor [" + sMode + "]");
    }
    
    public boolean isConnected(){
    	return socket.isConnected();
    	//return listener.checkrun();
    }
}
